package com.tom.demo.common.aop;
import com.tom.demo.common.annotation.OperaLog;
import com.tom.demo.common.enumeration.OperaTypeEnum;
import com.tom.demo.common.esEntity.SysLogDocIndex;
import com.tom.demo.common.utils.SecurityUtils;
import com.tom.demo.service.async.AsyncService;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;

/**
 * @Aspect — 标注一个类为切面（Aspect）。
 * @Pointcut — 定义切点表达式（可复用）。
 * @Before — 目标方法执行之前执行通知（advice）。
 * @After — 目标方法执行之后（无论成功或异常）执行通知（finally 风格）。
 * @AfterReturning — 目标方法正常返回后执行通知（可接收返回值）。
 * @AfterThrowing — 目标方法抛出异常时执行通知（可接收异常对象）。
 * @Around — 可环绕地控制目标方法执行（最强大，能决定是否执行目标方法、修改返回值、捕获异常等）。
 * @DeclareParents — 引入增强（引入新的接口/实现到目标类上，通常用于对现有对象动态添加接口）。
 * @Order (或实现 Ordered 接口) — 指定切面执行顺序（切面优先级）。
 * （补充说明：这些注解是 AspectJ 注解风格；Spring AOP 还支持 XML 配置或纯 AspectJ 切面织入）
 */
@Slf4j
@Component
@Aspect
@Order(value = 1) // 根据需要调整切面优先级
public class OperaLogAspect {

    @Resource
    private AsyncService asyncService;

    // ThreadLocal 存放每次方法调用的上下文
    private static final ThreadLocal<LogContext> LOG_CTX = new ThreadLocal<>();

    @Pointcut("@annotation(com.tom.demo.common.annotation.OperaLog)")
    public void sysLogPoint() {}

    /**
     * 可选的轻量 @Around 示例：如果你想短路或修改返回值，可以在这里处理。
     * 当前审计日志场景不需要环绕来控制是否执行目标方法，所以这个环绕示例是可选的，
     * 若不要，请删除此方法。此处保留一个透明的环绕用于记录进入/退出（不负责日志持久化）。
     */
    @Around("sysLogPoint()")
    public Object transparentAround(ProceedingJoinPoint pjp) throws Throwable {
        // 只是透明地继续执行，保留对 proceed 的控制权（例如你以后要实现限流/熔断）
        return pjp.proceed();
    }

    /**
     * 在方法执行前初始化上下文（保存到 ThreadLocal）
     */
    @Before("sysLogPoint()")
    public void before(JoinPoint joinPoint) {
        try {
            ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attrs == null ? null : attrs.getRequest();

            // 创建日志对象并设置基本信息（ID、请求信息、注解信息、用户信息、时间）
            SysLogDocIndex sysLog = new SysLogDocIndex();
            Optional.ofNullable(request).ifPresent(httpServletRequest->{
                sysLog.setRequestMethod(httpServletRequest.getMethod());
                sysLog.setRequestUrl(httpServletRequest.getRequestURI());
                sysLog.setRequestIp(getClientIp(httpServletRequest));
            });
            // 注解信息
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            OperaLog operaLog = method.getAnnotation(OperaLog.class);
            //如果operaLog不为null
            if(Optional.ofNullable(operaLog).isPresent()){
                sysLog.setOperaModel(operaLog.operaModel());
                OperaTypeEnum operaTypeEnum = operaLog.operaType();
                if(Optional.ofNullable(operaTypeEnum).isPresent()){
                    sysLog.setOperaType(operaTypeEnum.getType());
                    sysLog.setOperaCode(operaTypeEnum.getCode());
                }
                sysLog.setOperaDesc(operaLog.operaDesc());
            }
            // 时间（指定时区） Asia/Shanghai
            sysLog.setOperaDateTime(ZonedDateTime.now(ZoneId.of("Asia/Shanghai")));

            // 用户信息（可能为 null）
            String nickName = Optional.ofNullable(SecurityUtils.getLoginUser())
                    .map(u -> u.getSysUser().getNickName())
                    .orElse("");
            String userId = Optional.ofNullable(SecurityUtils.getLoginUser())
                    .map(u -> u.getSysUser().getUserId())
                    .orElse("");
            sysLog.setOperaNickName(nickName);
            sysLog.setOperaUserId(userId);

            // 保存到上下文
            LogContext ctx = new LogContext();
            ctx.setBeginTime(System.currentTimeMillis());
            ctx.setSysLog(sysLog);
            LOG_CTX.set(ctx);
            log.debug("OperaLogAspect.before initialized log context: id={}", sysLog.getId());
        } catch (Exception e) {
            // 不允许影响业务执行
            log.error("初始化操作日志上下文失败", e);
        }
    }

    /**
     * 方法正常返回时调用（记录成功信息）
     */
    @AfterReturning(pointcut = "sysLogPoint()", returning = "ret")
    public void afterReturning(JoinPoint joinPoint, Object ret) {
        LogContext ctx = LOG_CTX.get();
        if (ctx == null) {
            // before 阶段可能失败，防御性处理
            return;
        }
        try {
            long exec = System.currentTimeMillis() - ctx.getBeginTime();
            // 保持毫秒或按需转换；你原来转为秒，这里保持毫秒更精确
            // 如果你需要秒则改为 (int)(exec/1000)
            ctx.getSysLog().setOperaCostTime((int) exec); // 确保字段能够存毫秒或按需调整类型
            ctx.getSysLog().setOperaStatus(true);

            // 可选：记录返回值（注意敏感信息与大小）
            // ctx.getSysLog().setResponse(JsonUtil.toJson(ret));
            //异步保存信息
            asyncService.saveLogAsync(ctx.getSysLog(),null);
        } catch (Exception e) {
            log.error("处理成功日志时出错", e);
        }
    }

    /**
     * 方法抛出异常时调用（记录异常信息）
     */
    @AfterThrowing(pointcut = "sysLogPoint()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        LogContext ctx = LOG_CTX.get();
        if (ctx == null) {
            log.error("方法抛出异常但上下文未初始化", ex);
            return;
        }
        try {
            long exec = System.currentTimeMillis() - ctx.getBeginTime();
            ctx.getSysLog().setOperaCostTime((int) exec);
            ctx.getSysLog().setOperaStatus(false);

            String exMsg = ex == null ? "" : ex.getMessage();
            ctx.getSysLog().setExceptionMessage(exMsg);

            assert ex != null;
            String stack = ExceptionUtils.getStackTrace(ex);
            if (stack.length() > 4000) {
                stack = stack.substring(0, 4000);
            }
            ctx.getSysLog().setExceptionStack(stack);
            //异步保存信息
            asyncService.saveLogAsync(ctx.getSysLog(), ex);
        } catch (Exception e) {
            log.error("处理异常日志时出错", e);
        }
    }

    /**
     * 无论方法如何退出，最终清理 ThreadLocal
     */
    @After("sysLogPoint()")
    public void afterFinally(JoinPoint joinPoint) {
        LOG_CTX.remove();
    }

    /**
     * 获取客户端 IP（和你原始实现一致，注意代理情况）
     */
    public String getClientIp(HttpServletRequest request) {
        if (request == null) {
            return "";
        }
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }

    /**
     * 简单上下文对象
     */
    @Setter
    @Getter
    private static class LogContext {
        private long beginTime;
        private SysLogDocIndex sysLog;

    }
}
